/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification of Java bytecode.
 *
 * Copyright (c) 2002-2018 GuardSquare NV
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program; if not, write to the Free
 * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package proguard.util;

/**
 * This StringMatcher tests whether strings start with a specified variable string and then match another optional given
 * StringMatcher.
 *
 * @author Eric Lafortune
 */
public class VariableStringMatcher extends StringMatcher {
    private final char[] allowedCharacters;
    private final char[] disallowedCharacters;
    private final int minimumLength;
    private final int maximumLength;
    private final StringMatcher nextMatcher;

    // Remember the most recently attempted match.
    private String string;
    private int matchingBeginOffset;
    private int matchingEndOffset;
    private String matchingString;

    /**
     * Creates a new VariableStringMatcher.
     *
     * @param allowedCharacters an optional list of allowed characters.
     * @param disallowedCharacters an optional list of disallowed characters.
     * @param minimumLength the minimum length of te variable string.
     * @param maximumLength the maximum length of te variable string.
     * @param nextMatcher an optional next matcher for the remainder of the string.
     */
    public VariableStringMatcher(char[] allowedCharacters, char[] disallowedCharacters, int minimumLength,
        int maximumLength, StringMatcher nextMatcher) {
        this.allowedCharacters = allowedCharacters;
        this.disallowedCharacters = disallowedCharacters;
        this.minimumLength = minimumLength;
        this.maximumLength = maximumLength;
        this.nextMatcher = nextMatcher;
    }

    /**
     * Returns the string that has been matched most recently.
     */
    public String getMatchingString() {
        if (string == null) {
            throw new UnsupportedOperationException("Can't handle regular expression with referenced wildcard");
        }

        // Cache the matching string, since String#substring has become
        // expensive since about JRE 1.7.
        return matchingString == null ? (matchingString = string.substring(matchingBeginOffset, matchingEndOffset))
            : matchingString;
    }

    // Implementations for StringMatcher.

    @Override
    protected boolean matches(String string, int beginOffset, int endOffset) {
        int length = endOffset - beginOffset;

        // Handle the case without next matcher more efficiently.
        if (nextMatcher == null) {
            // Check the length and the characters.
            boolean match = length >= minimumLength && length <= maximumLength
                && areAllowedCharacters(string, beginOffset, endOffset);

            // Does the string match?
            if (match) {
                // Remember the matching string, so subsequent external
                // matchers can use it.
                rememberMatchingString(string, beginOffset, endOffset);
            } else {
                // Reset the matching string, so subsequent external
                // matchers cannot use it.
                resetMatchingString();
            }

            return match;
        }

        // Check the minimum length and the corresponding characters.
        if (length < minimumLength || !areAllowedCharacters(string, beginOffset, beginOffset + minimumLength)) {
            // Reset the matching string, so subsequent external
            // matchers cannot use it.
            resetMatchingString();

            return false;
        }

        int maximumLength = Math.min(this.maximumLength, length);

        // Check the remaining characters, up to the maximum number.
        for (int index = minimumLength; index < maximumLength; index++) {
            int offset = beginOffset + index;

            // Check the next matcher.
            if (matchesNextMatcher(string, beginOffset, offset, endOffset)) {
                return true;
            }

            // Otherwise just check the next character.
            if (!isAllowedCharacter(string.charAt(offset))) {
                // Reset the matching string, so subsequent external
                // matchers cannot use it.
                resetMatchingString();

                return false;
            }
        }

        // Last try: check the remaining characters in the string.
        int offset = beginOffset + maximumLength;

        // Check the next matcher.
        if (matchesNextMatcher(string, beginOffset, offset, endOffset)) {
            return true;
        }

        // Reset the matching string, so subsequent external
        // matchers cannot use it.
        resetMatchingString();

        return false;
    }

    // Small utility methods.

    /**
     * Returns whether the character characters in the specified substring are allowed.
     */
    private boolean areAllowedCharacters(String string, int beginOffset, int endOffset) {
        for (int offset = beginOffset; offset < endOffset; offset++) {
            if (!isAllowedCharacter(string.charAt(offset))) {
                return false;
            }
        }

        return true;
    }

    /**
     * Returns whether the given character is allowed in the variable string.
     */
    private boolean isAllowedCharacter(char character) {
        // Check the allowed characters.
        if (allowedCharacters != null) {
            for (int index = 0; index < allowedCharacters.length; index++) {
                if (allowedCharacters[index] == character) {
                    return true;
                }
            }

            return false;
        }

        // Check the disallowed characters.
        if (disallowedCharacters != null) {
            for (int index = 0; index < disallowedCharacters.length; index++) {
                if (disallowedCharacters[index] == character) {
                    return false;
                }
            }
        }

        // Any remaining character is allowed.
        return true;
    }

    /**
     * Returns whether the next matcher matches the specified second part of the string, remembering the first part that
     * matchers this matcher.
     */
    private boolean matchesNextMatcher(String string, int beginOffset, int splitOffset, int endOffset) {
        // Remember the matching string, so the next matchers can use it.
        rememberMatchingString(string, beginOffset, splitOffset);

        // Check the next matcher.
        return nextMatcher.matches(string, splitOffset, endOffset);
    }

    /**
     * Remembers the string that is currently being matched.
     */
    private void rememberMatchingString(String string, int matchingBeginOffset, int matchingEndOffset) {
        this.string = string;
        this.matchingBeginOffset = matchingBeginOffset;
        this.matchingEndOffset = matchingEndOffset;
        this.matchingString = null;
    }

    /**
     * Resets the string that is currently being matched.
     */
    private void resetMatchingString() {
        this.string = null;
        this.matchingBeginOffset = 0;
        this.matchingEndOffset = 0;
        this.matchingString = null;
    }
}
